package com.gel.shortLink.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gel.shortLink.common.constant.RedisCacheConstant;
import com.gel.shortLink.common.enums.ValidDateType;
import com.gel.shortLink.convention.exception.ServiceException;
import com.gel.shortLink.dao.entity.*;
import com.gel.shortLink.dao.mapper.*;
import com.gel.shortLink.dto.req.ShortLinkCreateReqDTO;
import com.gel.shortLink.dto.req.ShortLinkPageReqDTO;
import com.gel.shortLink.dto.req.ShortLinkUpdateReqDTO;
import com.gel.shortLink.dto.resp.ShortLinkCreateRespDTO;
import com.gel.shortLink.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.gel.shortLink.dto.resp.ShortLinkPageRespDTO;
import com.gel.shortLink.service.LinkDOService;
import com.gel.shortLink.util.HashUtil;
import com.gel.shortLink.common.constant.RedisCacheConstant.*;
import com.gel.shortLink.util.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.shaded.com.google.common.hash.BloomFilter;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.sql.Timestamp;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static com.gel.shortLink.common.constant.RedisCacheConstant.*;
import static com.gel.shortLink.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
* @author MJB_Computer
* @description 针对表【t_link_0】的数据库操作Service实现
* @createDate 2025-07-08 19:54:46
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class LinkDOServiceImpl extends ServiceImpl<LinkDOMapper, LinkDO>
    implements LinkDOService {

    @Value("${short-link.restapi.amapKey}")
    private String amapKey;
    @Value("${short-link.domain.default}")
    private String defaultDomain;
    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final LinkGotoMapper linkgotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {

        String shortUri= generateSuffix(requestParam);
        String fullShortUrl= StrBuilder.create(defaultDomain)
                .append("/")
                .append(shortUri)
                .toString();
        LinkDO linkDO = LinkDO.builder()
                .gid(requestParam.getGid())
                .domain(defaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .shortUri(shortUri)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .createdType(requestParam.getCreatedType())
                .describe(requestParam.getDescribe())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .build();
        try {
            baseMapper.insert(linkDO);
        }catch (DuplicateKeyException e){
            LambdaQueryWrapper<LinkDO> queryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getFullShortUrl, fullShortUrl);
            LinkDO hasshortLinkDO = baseMapper.selectOne(queryWrapper);
            if(hasshortLinkDO!=null){
                log.warn("短链接{}重复入库}",fullShortUrl);
                throw  new ServiceException("短链接重复");
            }
        }
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        LinkGotoDO linkGotoDO = LinkGotoDO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        linkgotoMapper.insert(linkGotoDO);

        stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()),TimeUnit.MILLISECONDS);

        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {

        IPage<LinkDO> pageLinks = baseMapper.pagLink(requestParam);
        return pageLinks.convert(each -> {
            ShortLinkPageRespDTO bean = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            return bean;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<LinkDO> queryWrapper = Wrappers.query(new LinkDO())
                .select("gid as gid , count(1) as shortLinkCount ")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .groupBy("gid");
        List<Map<String, Object>> maps = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(maps, ShortLinkGroupCountQueryRespDTO.class);

    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        //查询要修改的短链接是否存在
        LambdaQueryWrapper<LinkDO> queryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getGid, requestParam.getOriginGid())
                .eq(LinkDO::getDelFlag, 0)
                .eq(LinkDO::getEnableStatus, 0)
                .eq(LinkDO::getFullShortUrl, requestParam.getFullShortUrl());
        LinkDO haslinkDO = baseMapper.selectOne(queryWrapper);
        if(haslinkDO == null){
            throw new ServiceException("短链接记录不存在");
        }
        LinkDO linkDO = LinkDO.builder()
                .domain(haslinkDO.getDomain())
                .shortUri(haslinkDO.getShortUri())
                .clickNum(haslinkDO.getClickNum() )
                .favicon(haslinkDO.getFavicon())
                .gid(requestParam.getGid())
//                .fullShortUrl(requestParam.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .describe(requestParam.getDescribe())
                .createdType(haslinkDO.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .build();

        if(Objects.equals(haslinkDO.getGid(),requestParam.getGid())){
            LambdaUpdateWrapper<LinkDO> updateWrapper = Wrappers.lambdaUpdate(LinkDO.class)
                    .eq(LinkDO::getGid, requestParam.getGid())
                    .eq(LinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(LinkDO::getDelFlag, 0)
                    .eq(LinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), ValidDateType.PERMANENT.getType()), LinkDO::getValidDate, null);


            baseMapper.update(linkDO,updateWrapper);
        }else {
            LambdaQueryWrapper<LinkDO> updateWrapper = Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getGid, requestParam.getOriginGid())
                    .eq(LinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(LinkDO::getDelFlag, 0)
                    .eq(LinkDO::getDelTime, 0L)
                    .eq(LinkDO::getEnableStatus, 0);
            int delete = baseMapper.delete(updateWrapper);
            baseMapper.insert(linkDO);

        }
        //修改短连接后，对缓存进行操作，保证缓存一致性
        if(!Objects.equals(haslinkDO.getValidDateType(),requestParam.getValidDateType())
        || !Objects.equals(haslinkDO.getValidDate(),requestParam.getValidDate())
        ||!Objects.equals(haslinkDO.getOriginUrl(),requestParam.getOriginUrl())){
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            if(haslinkDO.getValidDate()!=null&&haslinkDO.getValidDate().before(new Date())){
                if(Objects.equals(haslinkDO.getValidDateType(),ValidDateType.PERMANENT)||requestParam.getValidDate().after(new Date())){
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }

            }
        }
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        //获取主机地址
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        //获取完整短链
        String fullShortUrl=serverName+serverPort+"/"+shortUri;
        //判断缓存中是否存在
        String OriginUrlLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(OriginUrlLink)){
            shortLinkStats(fullShortUrl,request,response);
            response.sendRedirect(OriginUrlLink);
            return;
        }
        //判断布隆过滤器中是否存在,不存在直接返回
        if(!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)){
            response.sendRedirect("/page/notfound");
            return;
        }
        //判断缓存中是否是空值，如果是空值直接返回
        String originUrlNull = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originUrlNull)){

            response.sendRedirect("/page/notfound");
            return;
        }
        //分布式锁查询数据库
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try{
            //双重判定锁，在锁内再次查询缓存（先获取锁的会将查询之后的结果存入缓存）
            String originUrlLinks =stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(originUrlLinks)){
                shortLinkStats(fullShortUrl,request,response);
                response.sendRedirect(originUrlLinks);
                return;
            }
            String originUrlNulls=stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl));
            if(StrUtil.isNotBlank(originUrlNulls)){
                response.sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<LinkGotoDO> queryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                    .eq(LinkGotoDO::getFullShortUrl, fullShortUrl);
            LinkGotoDO linkGotoDO = linkgotoMapper.selectOne(queryWrapper);
            if(linkGotoDO==null){
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl),"_");
                response.sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<LinkDO> queryWrapper1 = Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getGid, linkGotoDO.getGid())
                    .eq(LinkDO::getFullShortUrl, fullShortUrl)
                    .eq(LinkDO::getEnableStatus, 0)
                    .eq(LinkDO::getDelFlag, 0);
            LinkDO linkDO = baseMapper.selectOne(queryWrapper1);
            if(linkDO==null||(linkDO.getValidDate()!=null&&linkDO.getValidDate().before(new Date()))){
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl),"_");

                response.sendRedirect("/page/notfound");
                return;
            }
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    linkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(linkDO.getValidDate()), TimeUnit.MILLISECONDS
            );
            shortLinkStats(fullShortUrl,request,response);
            response.sendRedirect(linkDO.getOriginUrl());


        }finally {
            lock.unlock();
        }





    }
    /*短链接访问监控*/
    private void shortLinkStats(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        AtomicBoolean uvFirstFlag=new AtomicBoolean();
        Cookie[] cookies=request.getCookies();
        AtomicReference<String> uv=new AtomicReference<>();
        Date currentDate=new Date();
        Runnable addResponseCookieTask=()->{
            String actualUv=UUID.randomUUID().toString();
            uv.set(actualUv);
            Cookie cookie = new Cookie("uv",uv.get());
            cookie.setMaxAge(60*60*24*30);
            cookie.setPath(StrUtil.sub(fullShortUrl,fullShortUrl.indexOf("/"),fullShortUrl.length()));
            response.addCookie( cookie);
            uvFirstFlag.set(true);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY+fullShortUrl,uv.get());
        };
        if(ArrayUtil.isNotEmpty(cookies)){
            Arrays.stream(cookies).filter(each->Objects.equals(each.getName(),"uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each->{
                        uv.set(each);
                        Long uvadd = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvadd!=null&&uvadd>0L);
                    },addResponseCookieTask);
        }else {
            addResponseCookieTask.run();
        }
        String remoteAddr = request.getRemoteAddr();
        Long uipadd = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag=uipadd!=null&&uipadd>0L;


        try {
            LocalDateTime now = LocalDateTime.now();
            int hour = now.getHour();
            DayOfWeek weekday = now.getDayOfWeek();
            LinkAccessStatsDO build = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(uvFirstFlag.get()?1:0)
                    .uip(uipFirstFlag?1:0)
                    .fullShortUrl(fullShortUrl)
                    .weekday(weekday.getValue())
                    .hour(hour)
                    .date(Timestamp.valueOf(now))
                    .build();
            linkAccessStatsMapper.shortLinkStats(build);
            Map<String,Object> paramMap = new HashMap<>();
            paramMap.put("key",amapKey);
            paramMap.put("address",remoteAddr);
            String localResultStr = HttpUtil.get(AMAP_REMOTE_URL, paramMap);
            JSONObject jsonObject = JSON.parseObject(localResultStr);
            String infocode = jsonObject.getString("infocode");
            System.out.println("infocode:"+infocode);
            LinkLocaleStatsDO localeStatsDO;
            String actualProvince="未知";
            String actualCity="未知";
            if(StrUtil.isNotBlank(infocode)&&StrUtil.equals(infocode,"10000")){
                String province = jsonObject.getString("province");
                boolean unKnowFlag=StrUtil.equals(province,"[]");
                        localeStatsDO = LinkLocaleStatsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .province(actualProvince=unKnowFlag?"未知":province)
                        .city(actualCity=unKnowFlag?"未知":jsonObject.getString("city"))
                        .cnt(1)
                        .adcode(jsonObject.getString("adcode"))
                        .country("中国")
                        .date(currentDate)
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(localeStatsDO);
            }
            String osInfo = LinkUtil.getOsInfo(request);
            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                    .os(osInfo)
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
            String browserInfo = LinkUtil.getBrowserInfo(request);
            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                    .browser(browserInfo)
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkBrowserStatsMapper.shortLinkBrowserStats(linkBrowserStatsDO);
            String deviceType = LinkUtil.getDeviceType(request);
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .device(deviceType)
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkDeviceStatsMapper.shortLinkDeviceStats(linkDeviceStatsDO);
            String networkType = LinkUtil.getNetworkType(request);
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .network(networkType)
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);

            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .ip(remoteAddr)
                    .fullShortUrl(fullShortUrl)
                    .browser(browserInfo)
                    .os(osInfo)
                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                    .network(networkType)
                    .device(deviceType)
                    .user(uv.get())
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);
            LambdaQueryWrapper<LinkGotoDO> queryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                    .eq(LinkGotoDO::getFullShortUrl, fullShortUrl);
            LinkGotoDO linkGotoDO = linkgotoMapper.selectOne(queryWrapper);
            String gid = linkGotoDO.getGid();
            baseMapper.incrementStats(gid, fullShortUrl, 1, uvFirstFlag.get()?1:0, uipFirstFlag?1:0);
            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .todayPv(1)
                    .todayUv(uvFirstFlag.get() ? 1 : 0)
                    .todayUip(uipFirstFlag ? 1 : 0)
                    .date(currentDate)
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);


        } catch (Exception e) {
            log.error("访问监控失败",e);
        }

    }

    private String generateSuffix(ShortLinkCreateReqDTO requestParam){
        int count=0;
        String shortUri;

        while(true){
            if(count>10){
                throw new ServiceException("短链接生成失败，请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl+=System.currentTimeMillis();
            shortUri = HashUtil.hashToBase62(originUrl);
            String fullShortUrl = requestParam.getDomain() + "/" + shortUri;
            if(!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)){
                break;
            }
            count++;
        }
        return shortUri;


    }

    @SneakyThrows
    public String getFavicon(String originUrl) {
        URL targetUrl=new URL(originUrl);
        HttpURLConnection connection=(HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if(responseCode==HttpURLConnection.HTTP_OK){
            Document document = Jsoup.connect(originUrl).get();
            Element first = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if(first!=null){
                return first.attr("abs:href");
            }
        }
        return null;

    }
}




